import tkinter as tk
from   tkinter import ttk, filedialog, messagebox
from   PIL     import Image, ImageTk
from   qrcode  import QRCode
import os
import re
import io
import json
import time
import requests
import threading
import subprocess

# 软件版本号
version = 'V5.0'

class VideoDownloader:
    data_block = 1024 * 100
    download_file_path:dict[str, list[str, str]] = {}
    current_info = {'state': False, 'info': ''}
    cookies = None
    headers = {
        "Referer": "https://www.bilibili.com/",
        "Origin" : "https://www.bilibili.com/",
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
    }
    
    def __init__(self):
        if os.path.isfile('cookies.ini'):
            with open('cookies.ini', 'r') as f:
                self.cookies = requests.utils.cookiejar_from_dict(eval(f.read()))
        else:
            self.cookies = requests.utils.cookiejar_from_dict({})

    def getVideoInfo(self, url: str) -> dict:
        regist_filename = []
        def get_filename(name:str) -> str:
            name = safe_filename(name);extra_name:int = 0
            while  name + (f'({extra_name})' if extra_name > 0 else '') in regist_filename:extra_name += 1
            return name + (f'({extra_name})' if extra_name > 0 else '')
        def safe_filename(filename: str) -> str:
            return re.sub(r'[\\/:*?"<>|“‘’”？。…]', '', filename).replace('&quot;','')
        def get_p(url:str) -> int:
            try   : return int(re.search(r'(?:[?&]p=)([^&]*)', url).group(1) if re.search(r'[?&]p=', url) else '1')
            except: return 1
        def url_parse(url: str) -> str | None:
            try:
                response = requests.head(url, allow_redirects=True)
                if 'location' in response.headers:
                    return url_parse(response.headers['location'])
                else:
                    return re.findall(r'http.*?(?=\?|$)', response.url)[0] + '?p=' + str(get_p(response.url))
            except:
                return None
        try:
            app.parse_btn.config(state='disabled')
            app.video_btn.config(state='disabled')
            app.audio_btn.config(state='disabled')
            app.show_list_btn.config(state='disabled')
            app.url_entry.delete(0, 'end')
            app.url_entry.insert(0, re.findall(r'http.*?(?=\?|$)', url)[0] + '?p=' + str(get_p(url)))
            url = url_parse(re.findall(r'http.*?(?=\?|$)', url)[0] + '?p=' + str(get_p(url))) if 'http' in url else url
            if not "https://www.bilibili.com/video/BV" in url:
                app.parse_btn.config(state='normal')
                return {'state': False, 'info': "请输入正确的视频链接"}
            html = requests.get(url=url, headers=self.headers, cookies=self.cookies).text
            title = str(re.findall('title="(.*?)"', html)[0]).replace('&quot;','"')
            json_data = json.loads(re.findall('window.__playinfo__=(.*?)</script>', html)[0])
            video_dpi = (json_data['data']['dash']['video'][0]['width'], json_data['data']['dash']['video'][0]['height'])
            video_url = json_data['data']['dash']['video'][0]['baseUrl']
            audio_url = json_data['data']['dash']['audio'][0]['baseUrl']
            video_size = int(requests.get(url=video_url, headers=self.headers, cookies=self.cookies, stream=True).headers.get('Content-Length'))
            audio_size = int(requests.get(url=audio_url, headers=self.headers, cookies=self.cookies, stream=True).headers.get('Content-Length'))
            InitialState = json.loads(re.findall('window.__INITIAL_STATE__=(.*?)};', html)[0]+'}')
            if 'ugc_season' in InitialState['videoData'] or len(InitialState["availableVideoList"][0]['list']) > 1:
                ugc_season_title = InitialState['videoData']['ugc_season']['title'] if 'ugc_season' in InitialState['videoData'] else ''
                app.tree_list.checkbox_states = {}
                all_num_info = int(re.sub(r'[ （）]','', re.findall('<div class="amt" data-v-dac4fbd2>(.*?)</div>', html)[0]).split('/')[1])
                for i in app.tree_list.get_children(): app.tree_list.delete(i)
                for index in range(all_num_info):
                    item = InitialState["availableVideoList"][index]
                    app.tree_list.insert('', 'end', iid=item['bvid'], text=item['title'], open=True, values=(app.tree_list.checkbox_list[0],ugc_season_title,get_filename(item['title']),))
                    if len(item['list']) > 1:
                        for sub_item in item['list']:
                            app.tree_list.insert(
                                item['bvid'], 'end', iid=item['bvid']+'?p='+str(sub_item['p']), text=sub_item['title'],
                                values=(
                                    app.tree_list.checkbox_list[0],
                                    (ugc_season_title + '\\' if ugc_season_title else '') + safe_filename(item['title']),safe_filename(sub_item['title']),
                                )
                            )
                    else:   regist_filename.append(get_filename(item['title']))
                    if 'ugc_season' not in InitialState['videoData']:break
                app.show_list_btn.config(state='normal')
            # print(InitialState["availableVideoList"] # 所有合集信息
            # print(InitialState["availableVideoList"][index]['title']
            # print(InitialState["availableVideoList"][index]['bvid']
            # print(InitialState["videoData"]['ugc_season']['title'])  # 合集标题
            # print(InitialState["availableVideoList"][0]['title'])    # 分P 标题
            # print(InitialState["availableVideoList"][0]['list'][index]['title']) # 各个分P标题 (index从0开始到len(InitialState["availableVideoList"][0]['list'])-1)
        except Exception as e:
            app.parse_btn.config(state='normal')
            e = str(e) if not 'range'    in str(e) else '链接错误，请检查链接是否正确'
            return {'state': False, 'info': str(e)}
        app.parse_btn.config(state='normal')
        app.video_btn.config(state='normal')
        app.audio_btn.config(state='normal')
        self.current_info = {  'url': (video_url, audio_url), 'size': (video_size, audio_size), 'dpi': video_dpi, 'title': title}
        return {'state': True, 'url': (video_url, audio_url), 'size': (video_size, audio_size), 'dpi': video_dpi, 'title': title}
    
    def download(self, path: str = None, file_name: str = None, info:dict|str = None, only_audio: bool = False, progressFunc: callable = None, taskID = None):
        def system(cmd: str):
            return subprocess.Popen(cmd, shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
        
        def safe_filename(filename: str) -> str:
            return re.sub(r'[\\/:*?"<>|]', '', filename)

        app.tasks_tree.set(taskID, "status", "准备中")
        
        if isinstance(info, str):
            while True:
                try:
                    html = requests.get(url=info, headers=self.headers, cookies=self.cookies).text
                    title = re.findall('title="(.*?)"', html)[0]
                    json_data = json.loads(re.findall('window.__playinfo__=(.*?)</script>', html)[0])
                    video_url = json_data['data']['dash']['video'][0]['baseUrl']
                    audio_url = json_data['data']['dash']['audio'][0]['baseUrl']
                    video_size = int(requests.get(url=video_url, headers=self.headers, cookies=self.cookies, stream=True).headers.get('Content-Length'))
                    audio_size = int(requests.get(url=audio_url, headers=self.headers, cookies=self.cookies, stream=True).headers.get('Content-Length'))
                    info = {'url': (video_url, audio_url), 'size': (video_size, audio_size), 'title': title}
                except Exception as e:time.sleep(1)
                else:break

        file_name = safe_filename(info['title']) if not file_name else safe_filename(file_name)

        self.download_file_path[taskID] = (path, file_name + ('.mp4' if not only_audio else '.mp3'))

        if not os.path.exists(path):
            os.makedirs(path)
        
        if not only_audio:
            video_url  , audio_url  = info['url']
            video_size , audio_size = info['size']
            video_file = os.path.join(path, file_name + '[00].mp4')
            audio_file = os.path.join(path, file_name + '[01].aac')
            if os.path.isdir(video_file) or os.path.isdir(audio_file):
                if  progressFunc:
                    progressFunc(total=video_size + audio_size, current=0, taskID=taskID, success=False, info='下载失败：转存文件与文件夹重名')
                return
            try:
                if os.path.isfile(video_file):os.remove(video_file)
                if os.path.isfile(audio_file):os.remove(audio_file)
                if  progressFunc:
                    progressFunc(total=video_size + audio_size, current=0, taskID=taskID)
                
                with open(video_file, 'wb') as video_stream, open(audio_file, 'wb') as audio_stream:
                    video_response = requests.get(url=video_url, headers=self.headers, cookies=self.cookies, stream=True, timeout=(10, 20))
                    for data in video_response.iter_content(self.data_block):
                        video_stream.write(data)
                        if not app.tasks_tree.exists(taskID):
                            try:
                                video_stream.close()
                                audio_stream.close()
                                os.remove(video_file)
                                os.remove(audio_file)
                            except:pass
                            return
                        if  progressFunc:
                            progressFunc(total=video_size + audio_size, current=video_stream.tell(), taskID=taskID)
                        try:
                            while app.tasks_tree.item(taskID, "values")[3] == '暂停中':
                                time.sleep(0.5)
                        except:
                            if not app.tasks_tree.exists(taskID):
                                try:
                                    video_stream.close()
                                    audio_stream.close()
                                    os.remove(video_file)
                                    os.remove(audio_file)
                                except:pass
                                return
                    audio_response = requests.get(url=audio_url, headers=self.headers, cookies=self.cookies, stream=True, timeout=(10, 20))
                    for data in audio_response.iter_content(self.data_block):
                        audio_stream.write(data)
                        if not app.tasks_tree.exists(taskID):
                            try:
                                video_stream.close()
                                audio_stream.close()
                                os.remove(video_file)
                                os.remove(audio_file)
                            except:pass
                            return
                        if  progressFunc:
                            progressFunc(total=video_size + audio_size, current=video_stream.tell() + audio_stream.tell(), taskID=taskID)
                        try:
                            while app.tasks_tree.item(taskID, "values")[3] == '暂停中':
                                time.sleep(0.5)
                        except:
                            if not app.tasks_tree.exists(taskID):
                                try:
                                    video_stream.close()
                                    audio_stream.close()
                                    os.remove(video_file)
                                    os.remove(audio_file)
                                except:pass
                                return
                
                if  progressFunc:
                    progressFunc(total=video_size + audio_size, current=video_size + audio_size, taskID=taskID, success=False)
                if  os.path.isfile(os.path.join(path, file_name) + '.mp4'):os.remove(os.path.join(path, file_name) + '.mp4')
                system(f"ffmpeg -i \"{video_file}\" -i \"{audio_file}\" -c copy -metadata description=\"\" \"{os.path.join(path, file_name)}.mp4\" -y")
                os.remove(video_file)
                os.remove(audio_file)
                if  progressFunc:
                    progressFunc(total=video_size + audio_size, current=video_size + audio_size, taskID=taskID, success=True)
            except Exception as e:
                if  progressFunc:
                    progressFunc(total=video_size + audio_size, current=video_size + audio_size, taskID=taskID, success=False, info=str(e))
        else:
            audio_url  = info['url'] [1]
            audio_size = info['size'][1]
            audio_file = os.path.join(path, file_name + '.aac')
            if  os.path.isdir(audio_file):
                if  progressFunc:
                    progressFunc(total=audio_size, current=0, taskID=taskID, success=False, info='下载失败：转存文件与文件夹重名')
                return
            try:
                if os.path.isfile(audio_file):os.remove(audio_file)
                if  progressFunc:
                    progressFunc(total=audio_size, current=0, taskID=taskID)
                
                with open(audio_file, 'wb') as audio_stream:
                    audio_response = requests.get(url=audio_url, headers=self.headers, cookies=self.cookies, stream=True, timeout=(10, 20))
                    for data in audio_response.iter_content(self.data_block):
                        if not app.tasks_tree.exists(taskID):
                            try:
                                audio_stream.close()
                                os.remove(audio_file)
                            except:pass
                            return
                        audio_stream.write(data)
                        if progressFunc:
                            progressFunc(total=audio_size, current=audio_stream.tell(), taskID=taskID)
                        try:
                            while app.tasks_tree.item(taskID, "values")[3] == '暂停中':
                                time.sleep(0.5)
                        except:
                            if not app.tasks_tree.exists(taskID):
                                try:
                                    audio_stream.close()
                                    os.remove(audio_file)
                                except:pass
                                return
                
                if  progressFunc:
                    progressFunc(total=audio_size, current=audio_size, taskID=taskID, success=False)
                if  os.path.isfile(os.path.join(path, file_name) + '.mp3'):os.remove(os.path.join(path, file_name) + '.mp3')
                # system(f"ffmpeg -i \"{audio_file}\" -c libmp3lame -metadata title=\"{file_name}\" -metadata description=\"\" \"{os.path.join(path, file_name)}.mp3\" -y")
                process = os.popen(f"ffmpeg -i \"{audio_file}\" -c mp3 -metadata title=\"{file_name}\" -metadata description=\"\" \"{os.path.join(path, file_name)}.mp3\" -y -b:a 224k -write_xing 0 -id3v2_version 3 -progress pipe:1")
                
                def getStringValue(string:str, key:str, default:str='') -> str:
                    try:
                        start_index = string.index(key) + len(key)
                        end_index   = string.index('\n', start_index)
                        return string[start_index:end_index].strip()
                    except ValueError:return default

                def time_to_seconds(time_str: str) -> float:
                    try:
                        parts = time_str.split(':')
                        hours = float(parts[0])
                        minutes = float(parts[1])
                        seconds = float(parts[2])
                        return hours * 3600 + minutes * 60 + seconds
                    except (IndexError, ValueError):
                        return 0.0
                try:
                    ffprobe_command = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{audio_file}"'
                    duration = float(os.popen(ffprobe_command + '2>nul').read().strip()) if os.path.isfile(audio_file) else 0.0
                except:
                    duration = 0.0

                while True:
                    output_line = process.readline()
                    if not output_line:break
                    if  progressFunc:
                        CurrentTime = time_to_seconds(getStringValue(output_line, 'out_time=', '00:00:00.000'))
                        if CurrentTime == 0.0 or duration == 0.0 or CurrentTime > duration:continue
                        progressFunc(total=audio_size, current=min(int((CurrentTime / duration) * audio_size), audio_size), taskID=taskID, success=True)
                
                #os.remove(audio_file)
                if  progressFunc:
                    progressFunc(total=audio_size, current=audio_size, taskID=taskID, success=True)
            except Exception as e:
                if  progressFunc:
                    progressFunc(total=audio_size, current=audio_size, taskID=taskID, success=False, info=str(e))

class ToggleSwitch:
    def __init__(self, parent, width=80, height=40, on_color="#4CAF50", off_color="#e0e0e0", bg="#1a1b26", text_on_color="#FFA1A1", text_off_color="#66DD00",
                 thumb_color="#FFFFFF", border_color="#cccccc", animation_speed=5):
        self.parent = parent
        self.width = width
        self.height = height
        self.on_color = on_color
        self.off_color = off_color
        self.thumb_color = thumb_color
        self.border_color = border_color
        self.animation_speed = animation_speed
        self.state = False  # False = off, True = on
        
        # 计算尺寸和位置
        self.corner_radius = height // 2
        self.thumb_radius = (height - 8) // 2
        self.thumb_pos_off = self.corner_radius + 4
        self.thumb_pos_on = width - self.corner_radius - 4
        
        # 创建画布
        self.canvas = tk.Canvas(parent, width=width, height=height, bg=bg, highlightthickness=0)
        
        # 绘制开关背景
        self.bg_rect = self.canvas.create_rounded_rect(0, 0, width, height,
                                                      radius=self.corner_radius, 
                                                      fill=off_color, outline=border_color)
        
        # 绘制开关滑块
        self.thumb = self.canvas.create_oval(
            self.thumb_pos_off - self.thumb_radius, 
            4,
            self.thumb_pos_off + self.thumb_radius, 
            height - 4,
            fill=thumb_color, outline=border_color, width=1
        )
        
        # 添加状态文本
        self.text_off = self.canvas.create_text(
            width - self.corner_radius - 4, height/2, 
            text="OFF", fill=text_off_color, font=("Arial", 10, "bold")
        )
        self.text_on = self.canvas.create_text(
            self.corner_radius + 4, height/2, 
            text="ON", fill=text_on_color, font=("Arial", 10, "bold"), state=tk.HIDDEN
        )
        
        # 绑定事件
        self.canvas.bind("<Button-1>", self.toggle)
        self.canvas.bind("<Enter>", self.on_enter)
        self.canvas.bind("<Leave>", self.on_leave)
        
        # 动画相关变量
        self.animation_running = False
        self.animation_direction = 0
        self.current_thumb_pos = self.thumb_pos_off
        
    def create_rounded_rect(self, x1, y1, x2, y2, radius=10, **kwargs):
        """创建圆角矩形"""
        points = [x1+radius, y1,
                 x2-radius, y1,
                 x2, y1,
                 x2, y1+radius,
                 x2, y2-radius,
                 x2, y2,
                 x2-radius, y2,
                 x1+radius, y2,
                 x1, y2,
                 x1, y2-radius,
                 x1, y1+radius,
                 x1, y1]
        return self.canvas.create_polygon(points, smooth=True, **kwargs)
    
    def toggle(self, event=None):
        """切换开关状态"""
        if self.animation_running:
            return
            
        self.state = not self.state
        self.animate_thumb()
        
    def set_state(self, state):
        """设置开关状态"""
        if state != self.state and not self.animation_running:
            self.state = state
            self.animate_thumb()
            
    def get_state(self):
        """获取开关状态"""
        return self.state
    
    def on_enter(self, event):
        """鼠标进入时的效果"""
        self.canvas.config(cursor="hand2")
        
    def on_leave(self, event):
        """鼠标离开时的效果"""
        self.canvas.config(cursor="")
        
    def animate_thumb(self):
        """执行动画效果"""
        if self.animation_running:
            return
            
        self.animation_running = True
        target_pos = self.thumb_pos_on if self.state else self.thumb_pos_off
        
        # 更新背景颜色
        new_bg_color = self.on_color if self.state else self.off_color
        self.canvas.itemconfig(self.bg_rect, fill=new_bg_color)
        
        # 更新文本显示
        if self.state:
            self.canvas.itemconfig(self.text_off, state=tk.HIDDEN)
            self.canvas.itemconfig(self.text_on, state=tk.NORMAL)
        else:
            self.canvas.itemconfig(self.text_on, state=tk.HIDDEN)
            self.canvas.itemconfig(self.text_off, state=tk.NORMAL)
        
        # 执行动画
        self.animate_thumb_move(target_pos)
        
    def animate_thumb_move(self, target_pos):
        """移动滑块的动画"""
        if abs(self.current_thumb_pos - target_pos) < 0.5:
            self.current_thumb_pos = target_pos
            self.animation_running = False
            return
            
        # 计算移动方向和距离
        direction = 1 if target_pos > self.current_thumb_pos else -1
        distance = abs(target_pos - self.current_thumb_pos)
        step = min(self.animation_speed, distance) * direction
        
        self.current_thumb_pos += step
        
        # 移动滑块
        self.canvas.coords(
            self.thumb,
            self.current_thumb_pos - self.thumb_radius,
            4,
            self.current_thumb_pos + self.thumb_radius,
            self.height - 4
        )
        
        # 继续动画
        self.canvas.after(10, lambda: self.animate_thumb_move(target_pos))
        
    def pack(self, **kwargs):
        self.canvas.pack(**kwargs)
        
    def grid(self, **kwargs):
        self.canvas.grid(**kwargs)
        
    def place(self, **kwargs):
        self.canvas.place(**kwargs)

class CheckboxTreeview(ttk.Treeview):
    checkbox_list = '🔲🟩✅'
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        
        # 设置列
        self["columns"] = ("checkbox","path","name",)
        self.column("#0", width=500, minwidth=500)
        self.column("checkbox", width=20, minwidth=20, anchor="center")
        # 隐藏checkbox列
        self.column("path", width=0, minwidth=0, stretch=False)
        self.column("name", width=0, minwidth=0, stretch=False)
        
        # 设置列标题
        self.heading("#0", text="视频标题", anchor="center")
        self.heading("checkbox", text="选择", anchor="center")
        
        # 绑定点击事件
        self.bind("<Button-1>", self.on_click)
        
        # 存储复选框状态
        self.checkbox_states = {}
    
    def insert(self, parent, index, iid=None, **kw):
        # 插入项目时添加复选框
        item = super().insert(parent, index, iid=iid, **kw)
        self.set(item, "checkbox", self.checkbox_list[0])
        self.checkbox_states[item] = False
        return item
    
    def on_click(self, event):
        # 获取点击的区域
        region = self.identify_region(event.x, event.y)
        if region == "cell":
            # 获取点击的项目和列
            column = self.identify_column(event.x)
            item = self.identify_row(event.y)
            
            if column == "#1":  # 复选框列
                # 切换复选框状态
                current_state = self.checkbox_states[item]
                new_state = not current_state
                self.checkbox_states[item] = new_state
                
                # 更新复选框显示
                self.set(item, "checkbox", self.checkbox_list[2] if new_state else self.checkbox_list[0])
                
                # 如果有子项目，同步选择状态
                if self.get_children(item):
                    self.sync_children_state(item, new_state)
                
                # 如果有父项目，检查是否需要更新父项目状态
                parent = self.parent(item)
                if parent:
                    self.update_parent_state(parent)

    def sync_children_state(self, parent, state):
        # 同步子项目的选择状态
        for child in self.get_children(parent):
            self.checkbox_states[child] = state
            self.set(child, "checkbox", self.checkbox_list[2] if state else self.checkbox_list[0])
            
            # 递归同步子项目的子项目
            if self.get_children(child):
                self.sync_children_state(child, state)

    def update_parent_state(self, parent):
        # 更新父项目的选择状态
        children = self.get_children(parent)
        if not children:
            return
            
        # 检查所有子项目的状态
        all_checked = all(self.checkbox_states[child] for child in children)
        any_checked = any(self.checkbox_states[child] for child in children)
        
        # 设置父项目的状态
        if all_checked:
            self.checkbox_states[parent] = True
            self.set(parent, "checkbox", self.checkbox_list[2])
        elif any_checked:
            self.checkbox_states[parent] = False  # 部分选中状态
            self.set(parent, "checkbox", self.checkbox_list[1])
        else:
            self.checkbox_states[parent] = False
            self.set(parent, "checkbox", self.checkbox_list[0])
        
        # 递归更新父项目的父项目
        grandparent = self.parent(parent)
        if grandparent:
            self.update_parent_state(grandparent)

class CheckLogin(threading.Thread):
    def __init__(self, qrcode_key, session, update_callback):
        threading.Thread.__init__(self)
        self.qrcode_key = qrcode_key
        self.session = session
        self.update_callback = update_callback
        self.__run__ = True
        
    def destory(self):
        self.__run__ = False
        
    def run(self):
        check_login_url = f'https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={self.qrcode_key}&source=main-fe-header'
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0',
            'Referer': 'https://www.bilibili.com/',
            'Origin': 'https://www.bilibili.com'
        }
        
        while self.__run__:
            try:
                data = self.session.get(url=check_login_url, headers=headers).json()
                if data['data']['code'] == 0:
                    # 登录成功
                    self.session.get('https://www.bilibili.com/', headers=headers)
                    with open('cookies.ini', 'w') as c: 
                        c.write(str(self.session.cookies.get_dict()))
                    app.show_download_page()
                    app.notebook.tab(2, state='hidden')
                    if 'b23' in app.url_entry.get() or \
                       'BV'  in app.url_entry.get(): threading.Thread(target=app.parse_url,daemon=True).start()
                    self.update_callback(True, "登录成功！")
                    break
                elif data['data']['code'] == 86038:
                    # 二维码已过期
                    self.update_callback(False, "二维码已过期，请刷新二维码")
                    break
                elif data['data']['code'] == 86090:
                    # 二维码已扫描未确认
                    self.update_callback(None, "二维码已扫描，请在手机上确认登录")
                elif data['data']['code'] == 86101:
                    # 未扫描
                    pass
            except Exception as e:
                self.update_callback(False, f"登录检查错误: {str(e)}")
                break
            time.sleep(0.5)

class BiliBiliDownloader:
    tasks_CMID = 0
    tasks_tree = None
    tasks_thread = {}
    config_data  = {
        'auto_download_overwrite_without_ask': False,
        'auto_download_video': False,
        'auto_download_audio': False,
        'download_path'  : os.path.join(os.getcwd() if 'Program Files' not in os.getcwd() else os.path.expanduser('~'), 'Downloads'),
        'maximum_threads': 3,
    }
    last_clipboard_url = ''
    available_shutdown = False
    auto_download_video_var = False
    auto_download_audio_var = False
    def __init__(self, root):
        self.root     = root
        screen_width  = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()

        x = int(max(0, (screen_width - 1000) // 4))
        y = int(max(0, (screen_height - 600) // 4))

        self.root.title("Open Bilibili Downloader {}".format(version))
        self.root.geometry("1000x600+{}+{}".format(x, y))
        self.root.minsize(1000, 600)
        
        self.config_init()
        self.maximum_threads_var = tk.IntVar(value=self.config_data['maximum_threads'])
        self.downloader = VideoDownloader()
        
        self.setup_dark_theme()
        self.setup_styles()
        self.setup_layout()
        self.setup_download_page()
        self.setup_tasks_page()
        self.setup_login_page()
        self.setup_list_page()
        self.setup_settings_page()
        self.setup_about_page()
        self.stats_update()
        self.download_manager()
        self.always_get_link_by_clipboard()

    def config_init(self):
        if os.path.isfile('config.json'):
            try:
                with open('config.json', 'r', encoding='utf-8') as f:
                    for key, value in json.load(f).items():
                        self.config_data[key] = value
                try:
                    with open('config.json', 'w', encoding='utf-8') as f:
                        json.dump(self.config_data, f, indent=4, ensure_ascii=False)
                except Exception as e:
                    print(f"Error updating config file: {e}")
            except:
                with open('config.json', 'w', encoding='utf-8') as f:
                    self.config_data = {
                        'auto_download_overwrite_without_ask': False,
                        'auto_download_video': False,
                        'auto_download_audio': False,
                        'download_path'  : os.path.join(os.getcwd() if 'Program Files' not in os.getcwd() else os.path.expanduser('~'), 'Downloads'),
                        'maximum_threads': 3,
                    }
                    json.dump(self.config_data, f, indent=4, ensure_ascii=False)
        else:
            try:
                with open('config.json', 'w', encoding='utf-8') as f:
                    json.dump(self.config_data, f, indent=4, ensure_ascii=False)
            except Exception as e:
                print(f"Error creating config file: {e}")

    def set_config(self, key, value):
        self.config_data[key] = value
        self.write_config()

    def write_config(self):
        with open('config.json', 'w', encoding='utf-8') as f:
            json.dump(self.config_data, f, indent=4, ensure_ascii=False)

    def setup_dark_theme(self):
        self.bg_color = "#1a1b26"
        self.sidebar_color = "#16161e"
        self.card_color = "#24283b"
        self.text_color = "#a9b1d6"
        self.accent_color = "#7aa2f7"
        self.success_color = "#9ece6a"
        self.warning_color = "#e0af68"
        self.error_color = "#f7768e"
        self.root.configure(bg=self.bg_color)
    
    def setup_styles(self):
        style = ttk.Style()
        style.theme_use('clam')
        style.configure('.', background=self.bg_color, foreground=self.text_color)
        style.configure('TFrame', background=self.bg_color)
        style.configure('TLabel', background=self.bg_color, foreground=self.text_color)
        style.configure('TEntry', fieldbackground=self.card_color, foreground=self.text_color, 
                       borderwidth=1, relief='flat', padding=5)
        style.configure('TSpinbox', fieldbackground=self.card_color, foreground=self.text_color, 
                       borderwidth=1, relief='flat', padding=5)
        style.configure('TButton', background=self.card_color, foreground=self.text_color,
                       borderwidth=1, relief='flat', padding=(10, 5))
        style.configure('TLabelframe', background=self.bg_color, foreground=self.accent_color)
        style.configure('TLabelframe.Label', background=self.bg_color, foreground=self.accent_color)
        
        style.configure('Nav.TButton', font=('Microsoft YaHei', 10), padding=10, 
                       background=self.sidebar_color, foreground=self.text_color)
        style.configure('Title.TLabel', font=('Microsoft YaHei', 12, 'bold'), foreground=self.accent_color)
        style.configure('Normal.TLabel', font=('Microsoft YaHei', 9))
        style.configure('Normal.TEntry', font=('Microsoft YaHei', 9), fieldbackground="#2a2e3f", 
                       bordercolor="#414868", focuscolor="#414868")
        style.configure('Normal.TSpinbox', font=('Microsoft YaHei', 9), fieldbackground="#2a2e3f",
                        bordercolor="#414868", focuscolor="#414868")
        style.configure('Action.TButton', font=('Microsoft YaHei', 10), background="#007799", 
                       foreground='white', borderwidth=0, focuscolor=self.accent_color)
        style.configure('Secondary.TButton', font=('Microsoft YaHei', 9), background="#414868", 
                       foreground=self.text_color, borderwidth=0)
        style.configure('VideoList.TButton', font=('Microsoft YaHei', 10), background="#7700BB",
                       foreground='white', borderwidth=0)
        style.configure('FDisabled.TButton', font=('Microsoft YaHei', 9), background="#880000",
                       foreground='white', borderwidth=0)
        style.configure('FEnabled.TButton', font=('Microsoft YaHei', 9), background="#227700",
                       foreground='white', borderwidth=0)
        style.configure('User.TLabel', font=('Microsoft YaHei', 10), foreground=self.text_color)
        
        style.configure('Disabled.TButton', font=('Microsoft YaHei', 10), background="#343b58", 
                       foreground="#565f89", borderwidth=0)
        
        style.configure('Treeview', 
                       background=self.card_color,
                       foreground=self.text_color,
                       fieldbackground=self.card_color,
                       borderwidth=0,
                       rowheight=25)
        style.map('Treeview', background=[('selected', self.accent_color)])
        style.configure('Treeview.Heading', 
                       background=self.sidebar_color,
                       foreground=self.text_color,
                       relief='flat',
                       font=('Microsoft YaHei', 9, 'bold'))
        
        style.configure('TNotebook', 
                       background=self.bg_color,
                       tabmargins=[0, 0, 0, 0])
        style.configure('TNotebook.Tab', 
                       background=self.sidebar_color,
                       foreground=self.text_color,
                       padding=[15, 5],
                       font=('Microsoft YaHei', 10),
                       focuscolor=self.bg_color)
        style.map('TNotebook.Tab', 
                 background=[('selected', self.bg_color), ('active', '#2a2e3f')],
                 foreground=[('selected', self.accent_color), ('active', self.accent_color)])
        
        style.map('Action.TButton',
                 background=[('active', '#114488'), ('pressed', '#2055a8')])
        style.map('Secondary.TButton',
                 background=[('active', '#51587a'), ('pressed', '#484e6d')])
        style.map('VideoList.TButton',
                 background=[('active', "#3A0088"), ('pressed', '#550088')])
        style.map('FDisabled.TButton',
                 background=[('active', "#A42D00"), ('pressed', '#A42D00')])
        style.map('FEnabled.TButton',
                 background=[('active', "#008800"), ('pressed', '#008800')])
        style.map('TButton',
                 background=[('active', '#343b58'), ('pressed', '#2d334d')])
        style.map('Nav.TButton',
                 background=[('active', '#1f2233'), ('pressed', '#191c2a')])
        
        # 垂直滚动条基础样式配置
        style.configure("TScrollbar",
                        background="#1E1E1E",      # 滑块颜色 - 浅灰色
                        troughcolor="#1E1E1E",     # 滑槽颜色 - 接近黑色
                        bordercolor="#3E3E42",     # 边框颜色 - 中等灰色
                        arrowcolor="#FCFCFC",      # 箭头颜色 - 白色
                        gripcount=0)                 # 移除默认握把图案

        # 垂直滚动条动态效果配置
        style.map("TScrollbar",
                background=[('active', '#3E3E42'),    # 悬停时中等灰色
                            ('pressed', '#007ACC'),   # 按下时蓝色
                            ('disabled', '#505050')], # 禁用时深灰色
                arrowcolor=[('active', '#FFFFFF'),    # 悬停时白色
                            ('pressed', '#FFFFFF'),   # 按下时白色
                            ('disabled', '#707070')], # 禁用时灰色
                troughcolor=[('active', '#252526')])  # 悬停时滑槽稍亮
    
    def setup_layout(self):
        self.main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        self.main_paned.pack(fill=tk.BOTH, expand=True)
        
        style = ttk.Style()
        style.configure('Sidebar.TFrame', background=self.sidebar_color)
        self.nav_frame = ttk.Frame(self.main_paned, width=200, style='Sidebar.TFrame')
        self.nav_frame.pack_propagate(False)
        self.main_paned.add(self.nav_frame, weight=0)
        
        self.content_frame = ttk.Frame(self.main_paned, style='TFrame')
        self.main_paned.add(self.content_frame, weight=1)
        
        self.notebook = ttk.Notebook(self.content_frame, style='TNotebook')
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        self.download_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.download_page, text="下载页面")     # index 0
        
        self.tasks_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.tasks_page, text="任务管理")        # index 1

        self.login_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.login_page, text="账户登录")        # index 2

        self.list_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.list_page, text="合集列表")         # index 3
        
        self.settings_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.settings_page, text="软件设置")     # index 4

        self.about_page = ttk.Frame(self.notebook, style='TFrame')
        self.notebook.add(self.about_page, text="关于软件")        # index 5

        self.notebook.tab(1, state="hidden")
        self.notebook.tab(2, state="hidden")
        self.notebook.tab(3, state="hidden")
        self.notebook.tab(4, state="hidden")
        self.notebook.tab(5, state="hidden")
        
        self.setup_navigation()
    
    def setup_navigation(self):
        self.load_user_info()
        
        nav_buttons_frame = ttk.Frame(self.nav_frame, style='Sidebar.TFrame')
        nav_buttons_frame.pack(fill=tk.X, padx=10, pady=20)
        
        download_btn = ttk.Button(
            nav_buttons_frame, 
            text="📥 下载页面", 
            command=self.show_download_page,
            style='Nav.TButton'
        )
        download_btn.pack(fill=tk.X, pady=(0, 5))
        
        tasks_btn = ttk.Button(
            nav_buttons_frame, 
            text="📋 任务列表", 
            command=self.show_tasks_page,
            style='Nav.TButton'
        )
        tasks_btn.pack(fill=tk.X, pady=5)

        login_btn = ttk.Button(
            nav_buttons_frame, 
            text="👤 账户登录", 
            command=self.show_login_page,
            style='Nav.TButton'
        )
        login_btn.pack(fill=tk.X, pady=5)

        login_btn = ttk.Button(
            nav_buttons_frame, 
            text="⚙️ 软件设置", 
            command=self.show_settings_page,
            style='Nav.TButton'
        )
        login_btn.pack(fill=tk.X, pady=5)

        login_btn = ttk.Button(
            nav_buttons_frame, 
            text="💡 关于软件", 
            command=self.show_about_page,
            style='Nav.TButton'
        )
        login_btn.pack(fill=tk.X, pady=5)
        
        filler_frame = ttk.Frame(self.nav_frame, style='Sidebar.TFrame')
        filler_frame.pack(fill=tk.BOTH, expand=True)
        
        stats_frame = ttk.Frame(self.nav_frame, style='Sidebar.TFrame')
        stats_frame.pack(fill=tk.X, padx=15, pady=10, side=tk.BOTTOM)
        
        total_tasks = len(self.tasks_tree.get_children()) if self.tasks_tree else 0
        completed_tasks = sum(1 for item in self.tasks_tree.get_children() 
                            if self.tasks_tree.item(item, "values")[3] == "已完成") if self.tasks_tree else 0
        
        stats_text = f"总任务: {total_tasks}\n已完成: {completed_tasks}"
        self.stats_label = tk.Label(stats_frame, text=stats_text, 
                             fg=self.text_color, bg=self.sidebar_color,
                             font=('Microsoft YaHei', 9), justify=tk.LEFT)
        self.stats_label.pack(anchor=tk.W)
    
    def load_user_info(self):
        try:
            user_frame = ttk.Frame(self.nav_frame, style='Sidebar.TFrame')
            user_frame.pack(fill=tk.X, pady=20, padx=10, side=tk.TOP)
            name = ""
            
            try:
                html = requests.get('https://api.bilibili.com/x/web-interface/nav', 
                                   headers=self.downloader.headers, 
                                   cookies=self.downloader.cookies).json()
                if html['code'] == 0:
                    name = html['data']['uname']
                    avatar_url = html['data']['face']
                    try:
                        response = requests.get(avatar_url, timeout=5)
                        img_data = response.content
                    except:pass
            except:pass
            
            try:
                image = Image.open(io.BytesIO(img_data))
                image = image.resize((80, 80), Image.Resampling.LANCZOS)
                avatar_img = ImageTk.PhotoImage(image)
            except:
                image = Image.new('RGBA', (80, 80), (0, 0, 0, 0))
                avatar_img = ImageTk.PhotoImage(image)
            
            self.avatar_label = tk.Label(user_frame, image=avatar_img, bg=self.sidebar_color)
            self.avatar_label.image = avatar_img
            self.avatar_label.pack(pady=(0, 10))
            
            self.user_name_label = tk.Label(user_frame, text=name, 
                                     fg=self.text_color, bg=self.sidebar_color,
                                     font=('Microsoft YaHei', 12, 'bold'))
            self.user_name_label.pack(pady=(0, 5))
            
            status = "已登录" if name else "未登录"
            self.status_label = tk.Label(user_frame, text=status, 
                                  fg=self.accent_color if status == "已登录" else self.warning_color, 
                                  bg=self.sidebar_color,
                                  font=('Microsoft YaHei', 9))
            self.status_label.pack()
            
            separator = ttk.Separator(self.nav_frame, orient='horizontal')
            separator.pack(fill=tk.X, padx=10, pady=10, side=tk.TOP)
            
        except Exception as e:
            print(f"加载用户信息时出错: {e}")

    def update_user_info(self):
        try:
            name = "未登录"
            
            if os.path.isfile('cookies.ini'):
                with open('cookies.ini', 'r') as f:
                    self.downloader.cookies = requests.utils.cookiejar_from_dict(eval(f.read()))
            else:
                self.downloader.cookies = requests.utils.cookiejar_from_dict({})

            try:
                html = requests.get('https://api.bilibili.com/x/web-interface/nav', 
                                   headers=self.downloader.headers, 
                                   cookies=self.downloader.cookies).json()
                if html['code'] == 0:
                    name = html['data']['uname']
                    avatar_url = html['data']['face']
                    try:
                        response = requests.get(avatar_url, timeout=5)
                        img_data = response.content
                    except:pass
            except:pass
            
            try:
                image = Image.open(io.BytesIO(img_data))
                image = image.resize((80, 80), Image.Resampling.LANCZOS)
                avatar_img = ImageTk.PhotoImage(image)
            except:
                image = Image.new('RGBA', (80, 80), (0, 0, 0, 0))
                avatar_img = ImageTk.PhotoImage(image)
            
            self.avatar_label.config(image = avatar_img)
            self.avatar_label.image = avatar_img
            
            self.user_name_label.config(text=name)
            
            status = "已登录" if name != "未登录" else "未登录"
            self.status_label.config(text=status, fg=self.accent_color if name != "未登录" else self.warning_color)
            
        except Exception as e:
            print(f"加载用户信息时出错: {e}")
    
    def setup_download_page(self):
        card_frame = tk.Frame(self.download_page, bg=self.card_color, bd=0, relief='flat')
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        title_label = tk.Label(
            card_frame, 
            text="B站音视频下载器", 
            fg=self.accent_color, bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(20, 15))
        
        url_frame = tk.Frame(card_frame, bg=self.card_color)
        url_frame.pack(fill=tk.X, padx=20, pady=10)
        
        tk.Label(url_frame, text="视频链接:", fg=self.text_color, bg=self.card_color, 
                font=('Microsoft YaHei', 10)).pack(anchor=tk.W)
        
        self.url_entry = ttk.Entry(url_frame, style='Normal.TEntry')
        self.url_entry.pack(fill=tk.X, pady=(5, 0), ipady=3)

        add_right_click_menu_to_entry(self.url_entry)
        
        btn_group = tk.Frame(url_frame, bg=self.card_color)
        btn_group.pack(side=tk.TOP, pady=10)
        
        self.parse_btn = ttk.Button(
            btn_group, text="解析链接", 
            command=self.parse_url,
            style='Action.TButton'
        )
        self.parse_btn.pack(side=tk.LEFT, padx=5)

        self.video_btn = ttk.Button(
            btn_group, text="下载视频", 
            command=lambda: self.download(True),
            style='Action.TButton'
        )
        self.video_btn.pack(side=tk.LEFT, padx=5)
        self.video_btn.config(state='disabled')
        
        self.audio_btn = ttk.Button(
            btn_group, text="下载音频", 
             command=lambda: self.download(False),
            style='Action.TButton'
        )
        self.audio_btn.pack(side=tk.LEFT, padx=5)
        self.audio_btn.config(state='disabled')

        self.show_list_btn = ttk.Button(
            btn_group, text="合集/分P列表", 
             command=lambda: self.show_list_page(),
            style='VideoList.TButton'
        )
        self.show_list_btn.pack(side=tk.LEFT, padx=5)
        self.show_list_btn.config(state='disabled')
        
        info_frame = tk.Frame(card_frame, bg=self.card_color, bd=0, relief='flat')
        info_frame.pack(fill=tk.X, padx=20, pady=10)
        
        info_title = tk.Label(info_frame, text="视频信息", fg=self.accent_color, bg=self.card_color,
                             font=('Microsoft YaHei', 12, 'bold'))
        info_title.pack(anchor=tk.W, pady=(0, 10))
        
        title_info_frame = tk.Frame(info_frame, bg=self.card_color)
        title_info_frame.pack(fill=tk.X, pady=5)
        
        tk.Label(title_info_frame, text="视频标题:", fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT)
        self.title_var = tk.StringVar()
        tk.Label(title_info_frame, textvariable=self.title_var, fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT, padx=(5, 0))
        
        size_info_frame = tk.Frame(info_frame, bg=self.card_color)
        size_info_frame.pack(fill=tk.X, pady=5)
        
        tk.Label(size_info_frame, text="文件大小:", fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT)
        self.size_var = tk.StringVar()
        tk.Label(size_info_frame, textvariable=self.size_var, fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT, padx=(5, 0))
        
        dpi_info_frame = tk.Frame(info_frame, bg=self.card_color)
        dpi_info_frame.pack(fill=tk.X, pady=5)
        
        tk.Label(dpi_info_frame, text="视频分辨率:", fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT)
        self.dpi_var = tk.StringVar()
        tk.Label(dpi_info_frame, textvariable=self.dpi_var, fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT, padx=(5, 0))
        
        settings_frame = tk.Frame(card_frame, bg=self.card_color, bd=0, relief='flat')
        settings_frame.pack(fill=tk.X, padx=20, pady=10)
        
        settings_title = tk.Label(settings_frame, text="下载设置", fg=self.accent_color, bg=self.card_color,
                                 font=('Microsoft YaHei', 12, 'bold'))
        settings_title.pack(anchor=tk.W, pady=(0, 10))
        
        path_frame = tk.Frame(settings_frame, bg=self.card_color)
        path_frame.pack(fill=tk.X, pady=5)
        
        tk.Label(path_frame, text="保存路径:", fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT)
        
        self.path_var = tk.StringVar()
        self.path_var.set(self.config_data.get('download_path'))
        
        path_entry = ttk.Entry(path_frame, textvariable=self.path_var, style='Normal.TEntry')
        path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5), ipady=3)
        
        browse_btn = ttk.Button(path_frame, text="浏览", command=self.browse_path, style='Secondary.TButton')
        browse_btn.pack(side=tk.RIGHT)
        
        name_frame = tk.Frame(settings_frame, bg=self.card_color)
        name_frame.pack(fill=tk.X, pady=5)
        
        tk.Label(name_frame, text="文件名:   ", fg=self.text_color, bg=self.card_color,
                font=('Microsoft YaHei', 9)).pack(side=tk.LEFT)
        
        self.name_var = tk.StringVar()
        name_entry = ttk.Entry(name_frame, textvariable=self.name_var, style='Normal.TEntry')
        name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0), ipady=3)

        add_right_click_menu_to_entry(path_entry)
        add_right_click_menu_to_entry(name_entry)
    
    def setup_tasks_page(self):
        card_frame = tk.Frame(self.tasks_page, bg=self.card_color, bd=0, relief='flat')
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        title_label = tk.Label(
            card_frame, 
            text="下载任务", 
            fg=self.accent_color, bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(20, 15))
        
        list_frame = tk.Frame(card_frame, bg=self.card_color)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
        
        columns = ("name", "type", "progress", "status")
        self.tasks_tree = ttk.Treeview(
            list_frame, columns=columns, 
            show="headings", height=10
        )

        def on_task_double_click(event):
            app.open_file(self.tasks_tree.selection()[0])

        def on_task_right_click(event):
            iid = self.tasks_tree.identify_row(event.y)
            if iid:
                self.tasks_tree.selection_set(iid)
                task_menu = tk.Menu(self.root, tearoff=0)
                task_menu.add_command(label="打开文件", command=lambda: app.open_file(iid))
                task_menu.add_command(label="打开文件位置", command=lambda: app.open_download_folder(iid))
                task_menu.add_command(label="暂停/继续", command=lambda: app.manage_task(iid))
                task_menu.add_command(label="删除任务", command=lambda: app.delete_task(iid))
                task_menu.post(event.x_root, event.y_root)
        
        self.tasks_tree.bind("<Button-3>", on_task_right_click)
        self.tasks_tree.bind("<Double-Button-1>", on_task_double_click)
        
        self.tasks_tree.heading("name", text="任务名称")
        self.tasks_tree.heading("type", text="下载类型")
        self.tasks_tree.heading("progress", text="进度")
        self.tasks_tree.heading("status", text="状态")
        
        self.tasks_tree.column("name", width=300)
        self.tasks_tree.column("type", width=100)
        self.tasks_tree.column("progress", width=100)
        self.tasks_tree.column("status", width=100)
        
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tasks_tree.yview)
        self.tasks_tree.configure(yscrollcommand=scrollbar.set)
        
        self.tasks_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        control_frame = tk.Frame(card_frame, bg=self.card_color)
        control_frame.pack(pady=(0, 20))

        ttk.Button(control_frame, text="打开下载文件夹" , command=self.open_download_folder, style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="打开文件"  , command=self.open_file  , style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="暂停/继续" , command=self.manage_task, style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="删除任务"  , command=self.delete_task, style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="删除完成项", command=self.delete_done, style='Secondary.TButton').pack(side=tk.LEFT, padx=5)

    def setup_login_page(self):
        card_frame = tk.Frame(self.login_page, bg=self.card_color, bd=0, relief='flat')
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        title_label = tk.Label(
            card_frame, 
            text="B站账户登录", 
            fg=self.accent_color, bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(20, 15))
        
        # 警告信息
        warning_frame = tk.Frame(card_frame, bg=self.card_color)
        warning_frame.pack(fill=tk.X, padx=20, pady=10)
        
        warning_text = "⚠️ 请注意： 登录功能需要使用B站手机APP扫描二维码\n登录成功后，账户凭证Cookies将保存在本地，用于下载更高分辨率的视频"
        warning_label = tk.Label(warning_frame, text=warning_text, 
                               fg=self.warning_color, bg=self.card_color,
                               font=('Microsoft YaHei', 10), justify=tk.LEFT)
        warning_label.pack(anchor=tk.W)
        
        # 二维码显示区域
        qr_frame = tk.Frame(card_frame, bg=self.card_color)
        qr_frame.pack(pady=20)
        
        self.qr_label = tk.Label(qr_frame, bg=self.card_color)
        self.qr_label.pack()
        
        # 状态信息
        self.login_status = tk.StringVar()
        self.login_status.set("请使用B站APP扫描二维码登录")
        status_label = tk.Label(card_frame, textvariable=self.login_status,
                              fg=self.text_color, bg=self.card_color,
                              font=('Microsoft YaHei', 10))
        status_label.pack(pady=10)
        
        # 按钮区域
        btn_frame = tk.Frame(card_frame, bg=self.card_color)
        btn_frame.pack(pady=10)
        
        self.login_refresh_btn = ttk.Button(btn_frame, text="刷新二维码", command=lambda: threading.Thread(target=self.generate_qrcode, daemon=True).start(),
                  style='Action.TButton')
        self.login_refresh_btn.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(btn_frame, text="返回首页", command=self.show_download_page,
                  style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
    
    def generate_qrcode(self):
        try:
            self.login_status.set("正在生成二维码...")
            self.login_refresh_btn.config(state=tk.DISABLED)

            url = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header'
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0',
                'Referer': 'https://www.bilibili.com/',
                'Origin': 'https://www.bilibili.com'
            }
            
            response = requests.get(url=url, headers=headers).json()
            qrcode_url = response['data']['url']
            self.qrcode_key = response['data']['qrcode_key']
            
            # 生成二维码图片
            qr = QRCode()
            qr.add_data(qrcode_url)
            img = qr.make_image()
            img = img.resize((200, 200), Image.Resampling.LANCZOS)
            
            qr_img = ImageTk.PhotoImage(img)
            self.qr_label.config(image=qr_img)
            self.qr_label.image = qr_img
            
            # 启动登录检查线程
            self.session = requests.Session()
            if hasattr(self, 'login_thread'):
                self.login_thread.destory()
            
            self.login_thread = CheckLogin(self.qrcode_key, self.session, self.update_login_status)
            self.login_thread.daemon = True
            self.login_thread.start()
            self.login_status.set("请使用B站APP扫描二维码登录")
        except Exception as e:
            self.login_status.set(f"生成二维码失败，请检查网络连接或稍后重试")
            messagebox.showerror("错误", f"生成二维码失败: {e}")
        self.login_refresh_btn.config(state=tk.NORMAL)
    
    def update_login_status(self, success, message):
        if success is True:
            self.login_status.set(message)
            # 登录成功后重新加载用户信息
            self.update_user_info()
            messagebox.showinfo("登录成功", "登录成功！")
        elif success is False:
            self.login_status.set(message)
        else:
            self.login_status.set(message)
    
    def setup_list_page(self):
        card_frame = tk.Frame(self.list_page, bg=self.card_color, bd=0, relief='flat')
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        title_label = tk.Label(
            card_frame, 
            text="视频合集列表", 
            fg=self.accent_color, bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(20, 15))

        list_frame = tk.Frame(card_frame, bg=self.card_color)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)

        # 创建垂直滚动条
        v_scrollbar = ttk.Scrollbar(list_frame)
        v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # 创建树形视图
        self.tree_list = CheckboxTreeview(list_frame, show="tree headings", height=12)
        self.tree_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 配置滚动条
        v_scrollbar.config(command=self.tree_list.yview)
        self.tree_list.config(yscrollcommand=v_scrollbar.set)

        # 按钮区域
        btn_frame = tk.Frame(card_frame, bg=self.card_color)
        btn_frame.pack(side=tk.BOTTOM, pady=10)

        ttk.Button(btn_frame, text="返回首页", command=self.show_download_page,
                  style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        
        self.togge_select_all_btn = ttk.Button(btn_frame, text="全选/取消全选", command=lambda: threading.Thread(target=self.toggle_select_all, daemon=True).start(),
                  style='Secondary.TButton')
        self.togge_select_all_btn.pack(side=tk.LEFT, padx=5)
        
        ttk.Button(btn_frame, text="解析高亮选项", command=lambda: threading.Thread(target=self.analysis_selected, daemon=True).start(),
                  style='Secondary.TButton').pack(side=tk.LEFT, padx=5)

        ttk.Button(btn_frame, text="下载选中的视频", command=lambda: threading.Thread(target=self.download_selected, args=(True,), daemon=True).start(),
                  style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        
        ttk.Button(btn_frame, text="下载选中的音频", command=lambda: threading.Thread(target=self.download_selected, args=(False,), daemon=True).start(),
                  style='Secondary.TButton').pack(side=tk.LEFT, padx=5)
        
    def setup_settings_page(self):
        card_frame = tk.Frame(self.settings_page, bg=self.card_color, bd=0, relief='flat', padx=20, pady=20)
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 标题区域
        title_label = tk.Label(
            card_frame, 
            text="软件设置", 
            fg=self.accent_color, bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(0, 20))
        
        # 添加分隔线
        separator = ttk.Separator(card_frame, orient='horizontal')
        separator.pack(fill=tk.X, pady=(0, 25))
        
        # 设置项容器
        settings_frame = tk.Frame(card_frame, bg=self.card_color)
        settings_frame.pack(fill=tk.BOTH, expand=True)
        
        # 默认下载路径设置
        download_path_frame = tk.Frame(settings_frame, bg=self.card_color)
        download_path_frame.pack(fill=tk.X, pady=12)
        
        path_label = tk.Label(
            download_path_frame,
            text="默认下载路径：",
            fg=self.text_color,
            bg=self.card_color,
            font=('Microsoft YaHei', 12),
            width=12,
            anchor='w'
        )
        path_label.pack(side=tk.LEFT, padx=(20, 10))

        path_entry_var = tk.StringVar(value=self.config_data.get('download_path'))
        path_entry = ttk.Entry(download_path_frame, textvariable=path_entry_var, style='Normal.TEntry')
        path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), pady=2, ipady=3)
        path_entry_var.trace_add('write', lambda *args: self.set_config('download_path',path_entry_var.get()))

        add_right_click_menu_to_entry(path_entry)

        def browse_path():
            selected_path = filedialog.askdirectory().replace('/', '\\')
            if selected_path:
                path_entry_var.set(selected_path)
                self.set_config  ('download_path', selected_path)

        browse_btn = ttk.Button(
            download_path_frame,text="浏览",
            style='Secondary.TButton',
            command=browse_path
        )
        browse_btn.pack(side=tk.RIGHT)
        
        # 同时下载线程数设置
        threads_frame = tk.Frame(settings_frame, bg=self.card_color)
        threads_frame.pack(fill=tk.X, pady=12)
        
        threads_label = tk.Label(
            threads_frame,
            text="同时下载线程：",
            fg=self.text_color,
            bg=self.card_color,
            font=('Microsoft YaHei', 12),
            width=12,
            anchor='w'
        )
        threads_label.pack(side=tk.LEFT, padx=(20, 10))
        
        threads_spinbox = ttk.Spinbox(
            threads_frame,
            textvariable=self.maximum_threads_var,
            from_=1,to=24,increment=1,
            style='Normal.TSpinbox',
            command=self.update_max_threads
        )
        threads_spinbox.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), pady=2)

        self.maximum_threads_var.trace_add('write', lambda *args: self.update_max_threads())

        ttk.Button(threads_frame, text="减小", style='Secondary.TButton', command=lambda: self.maximum_threads_var.set(self.maximum_threads_var.get() - 1 if self.maximum_threads_var.get() > 1  else 1 )).pack(side=tk.RIGHT)
        ttk.Button(threads_frame, text="增加", style='Secondary.TButton', command=lambda: self.maximum_threads_var.set(self.maximum_threads_var.get() + 1 if self.maximum_threads_var.get() < 24 else 24)).pack(side=tk.RIGHT, padx=(0, 10))

        ##########################################################################

        def Exchange(_object_:str):
            if _object_ == 'video':
                app.set_config('auto_download_video', not self.auto_download_video_var)
                self.auto_download_video_var = not self.auto_download_video_var
                self.auto_download_video_btn.config(style='FEnabled.TButton' if self.auto_download_video_var else 'FDisabled.TButton')
            if _object_ == 'audio':
                app.set_config('auto_download_audio', not self.auto_download_audio_var)
                self.auto_download_audio_var = not self.auto_download_audio_var
                self.auto_download_audio_btn.config(style='FEnabled.TButton' if self.auto_download_audio_var else 'FDisabled.TButton')

        self.auto_download_video_var = self.config_data.get('auto_download_video', False)
        self.auto_download_audio_var = self.config_data.get('auto_download_audio', False)

        auto_download_frame = tk.Frame(settings_frame, bg=self.card_color)
        auto_download_frame.pack(fill=tk.X, pady=12)

        auto_download_label = tk.Label(
            auto_download_frame,
            text="在解析链接后：",
            fg=self.text_color,
            bg=self.card_color,
            font=('Microsoft YaHei', 12),
            width=12,
            anchor='w'
        )
        auto_download_label.pack(side=tk.LEFT, padx=(20, 10))

        self.auto_download_video_btn = ttk.Button(
            auto_download_frame,
            style='FDisabled.TButton' if not self.auto_download_video_var else 'FEnabled.TButton',
            text='下载视频',width=12,
            command=lambda: Exchange('video')
        )
        self.auto_download_video_btn.pack(side=tk.LEFT, padx=(0, 10))

        self.auto_download_audio_btn = ttk.Button(
            auto_download_frame,
            style='FDisabled.TButton' if not self.auto_download_audio_var else 'FEnabled.TButton',
            text='下载音频',width=12,
            command=lambda: Exchange('audio')
        )
        self.auto_download_audio_btn.pack(side=tk.LEFT, padx=(0, 10))

        def Exchange_Overwrite_Btn():
            if self.config_data.get('auto_download_overwrite_without_ask', False):
                app.set_config('auto_download_overwrite_without_ask', False)
                self.auto_download_overwrite_without_ask_btn.config(style='FDisabled.TButton')
            else:
                app.set_config('auto_download_overwrite_without_ask', True)
                self.auto_download_overwrite_without_ask_btn.config(style='FEnabled.TButton')

        self.auto_download_overwrite_without_ask_btn = ttk.Button(
            auto_download_frame,
            style='FDisabled.TButton' if not self.config_data.get('auto_download_overwrite_without_ask', False) else 'FEnabled.TButton',
            text='无需询问直接覆盖重名文件',
            command=Exchange_Overwrite_Btn
        )
        self.auto_download_overwrite_without_ask_btn.pack(side=tk.RIGHT, fill=tk.X, expand=True)

        #########################################################################

        shutdown_after_download_frame = tk.Frame(settings_frame, bg=self.card_color)
        shutdown_after_download_frame.pack(fill=tk.X, pady=12)

        path_label = tk.Label(
            shutdown_after_download_frame,
            text="下载完成后关机：",
            fg=self.text_color,
            bg=self.card_color,
            font=('Microsoft YaHei', 12),
            width=12,
            anchor='w'
        )
        path_label.pack(side=tk.LEFT, padx=(20, 10))

        self.shutdown_after_download_btn = ToggleSwitch(
            shutdown_after_download_frame,
            thumb_color=self.accent_color,
            border_color=self.card_color,width=105,height=32,
            on_color="#227700", off_color="#A42D00",
        )
        self.shutdown_after_download_btn.pack(side=tk.LEFT, padx=(0, 10))

        # 底部说明文字
        note_label = tk.Label(
            card_frame,
            text="部分设置更改后在重启应用时生效",
            fg=self.text_color,
            bg=self.card_color,
            font=('Microsoft YaHei', 9),
            justify='center'
        )
        note_label.pack(side=tk.BOTTOM, pady=(20, 0))

    def update_max_threads(self):
        self.maximum_threads_var.set(max(1, min(24, self.maximum_threads_var.get())))
        self.config_data['maximum_threads'] = self.maximum_threads_var.get()
        self.set_config ('maximum_threads'  , self.maximum_threads_var.get())
    
    def setup_about_page(self):
        card_frame = tk.Frame(self.about_page, bg=self.card_color, bd=0, relief='flat', padx=20, pady=20)
        card_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 标题区域 - 增加分隔线和更优雅的间距
        title_label = tk.Label(
            card_frame, 
            text="关于软件", 
            fg=self.accent_color, 
            bg=self.card_color,
            font=('Microsoft YaHei', 16, 'bold')
        )
        title_label.pack(pady=(0, 20))
        
        # 添加分隔线
        separator = ttk.Separator(card_frame, orient='horizontal')
        separator.pack(fill=tk.X, pady=(0, 25))
        
        info_frame = tk.Frame(card_frame, bg=self.card_color)
        info_frame.pack(fill=tk.BOTH, expand=True)
        
        # 定义信息项配置
        info_config = [
            {"label": "软件名称：", "value": f"Open Bilibili Downloader  {version}"},
            {"label": "开发者："  , "value": "Linmengen (3177738735@qq.com)"},
            {"label": "项目地址：", "value": "https://gitee.com/LinmengenData/open-bilibili-downloader"},
            {"label": "使用许可：", "value": "MIT License"},
            {"label": "开发语言：", "value": "Python 3.8+"},
            {"label": "系统要求：", "value": "Windows NT"},
        ]

        def copy_to_clipboard(text):
            self.root.clipboard_clear()
            self.root.clipboard_append(text)
            messagebox.showinfo("已复制", f"已将内容复制到剪贴板:\n{text}")
        
        def create_item(item):
            row_frame = tk.Frame(info_frame, bg=self.card_color)
            row_frame.pack(fill=tk.X, pady=4)
            
            label = tk.Label(
                row_frame, 
                text=item['label'], 
                fg=self.accent_color, 
                bg=self.card_color, 
                font=('Microsoft YaHei', 10, 'bold'),
                width=10,
                anchor='e'
            )
            label.pack(side=tk.LEFT, padx=(0, 5))
            
            value = tk.Label(
                row_frame, 
                text=item['value'], 
                fg=self.text_color, 
                bg=self.card_color, 
                font=('Microsoft YaHei', 10)
            )
            value.pack(side=tk.LEFT)

            menu = tk.Menu(value, tearoff=0)
            menu.add_command(label="复制", command=lambda: copy_to_clipboard(item['value']))

            value.bind("<Button-3>", lambda event: menu.post(event.x_root, event.y_root))

        # 创建并放置标签 - 使用更一致的间距和对齐
        for item in info_config:create_item(item)
        
        # 添加底部版权信息 - 更优雅的呈现方式
        bottom_frame = tk.Frame(card_frame, bg=self.card_color)
        bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(20, 0))
        
        # 添加另一条分隔线
        bottom_separator = ttk.Separator(bottom_frame, orient='horizontal')
        bottom_separator.pack(fill=tk.X, pady=(0, 15))
        
        copyright_label = tk.Label(
            bottom_frame, 
            text="© 2025 Open Bilibili Downloader. All rights reserved.",
            fg=self.text_color, 
            bg=self.card_color,
            font=('Microsoft YaHei', 9)
        )
        copyright_label.pack(pady=5)
    
    def toggle_select_all(self):
        """切换全选/取消全选状态"""
        self.togge_select_all_btn.config(state=tk.DISABLED)
        
        checkbox_list   = self.tree_list.checkbox_list
        checkbox_states = self.tree_list.checkbox_states
        
        # 有任何一个未选中状态，则全部选中；否则全部取消选中
        new_state = not all(checkbox_states.values())
        for item in checkbox_states:
            if checkbox_states[item] != new_state:
                self.tree_list.set(item, "checkbox", checkbox_list[2] if new_state else checkbox_list[0])
            checkbox_states[item] = new_state
        self.togge_select_all_btn.config(state=tk.NORMAL)

    def download_selected(self, isvideo:bool=True):
        selected_items = []
        for item, checked in self.tree_list.checkbox_states.items():
            if checked and not self.tree_list.get_children(item):
                selected_items.append(item)
        if not  selected_items:selected_items = [x for x in self.tree_list.selection() if not self.tree_list.get_children(x)]
        if not  selected_items:
            messagebox.showwarning("警告", "请先选择一个{}".format("视频" if isvideo else "音频"))
            return
        if  len(self.tree_list.selection()) > 1:
            selected_items = list(set(selected_items) | {x for x in self.tree_list.selection() if not self.tree_list.get_children(x)})
        for item in selected_items:
            path = self.path_var.get()+self.tree_list.item(item, "values")[1] if self.path_var.get()[-1] == '\\' else self.path_var.get()+'\\'+self.tree_list.item(item, "values")[1]
            name = self.tree_list.item(item, "values")[2]
            if not os.path.exists(path):
                os.makedirs (path)
            self.create_task(path=path, name=re.sub(r'[\\/:*?"<>|“‘’”？。…]', '', name), is_video=isvideo, info='https://www.bilibili.com/video/'+item)
        self.show_tasks_page()

    def analysis_selected(self):
        selected = self.tree_list.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一个视频")
            return
        if len(selected) > 1:
            messagebox.showwarning("警告", "只能选择一个视频")
            return
        self.show_download_page()
        self.parse_url(f"https://www.bilibili.com/video/{selected[0]}")

    def show_download_page(self):
        self.notebook.select(self.download_page)
    
    def show_tasks_page(self):
        self.notebook.select(self.tasks_page)

    def show_login_page(self):
        if not hasattr(self, 'login_thread'):
            self.generate_qrcode()
        self.notebook.select(self.login_page)

    def show_list_page(self):
        self.notebook.select(self.list_page)

    def show_settings_page(self):
        self.notebook.select(self.settings_page)

    def show_about_page(self):
        self.notebook.select(self.about_page)
    
    def always_get_link_by_clipboard(self):
        def get_p(url:str) -> int:
            try   : return int(re.search(r'(?:[?&]p=)([^&]*)', url).group(1) if re.search(r'[?&]p=', url) else '1')
            except: return 1
        try:
            url:str = self.root.clipboard_get()
            if  "https://www.bilibili.com/video/" in url or \
                "https://b23.tv/" in url:
                url = re.findall(r'http.*?(?=\?|$)', url)[0] + '?p=' + str(get_p(url))
                if url != self.last_clipboard_url:
                    self.last_clipboard_url = url
                    self.root.after(0, self.show_download_page)
                    self.root.after(0, self.parse_url, url)
        except:pass
        self.root.after(400, self.always_get_link_by_clipboard)

    def parse_url(self, url:str = None):
        url =  url if url else self.url_entry.get()
        if not url:
            messagebox.showerror("错误", "请输入视频链接")
            return
        
        def parse_thread():
            try:
                result = self.downloader.getVideoInfo(url)
                if result['state']:
                    self.root.after(0, self.update_video_info, result)
                else:
                    self.root.after(0, messagebox.showerror, "错误", f"解析失败: {result['info']}")
            except Exception as e:
                self.root.after(0, messagebox.showerror, "错误", f"解析失败: 未知错误")
        
        threading.Thread(target=parse_thread, daemon=True).start()
    
    def stats_update(self):
        total_tasks = len(self.tasks_tree.get_children()) if self.tasks_tree else 0
        completed_tasks = sum(1 for item in self.tasks_tree.get_children() 
                            if self.tasks_tree.item(item, "values")[3] == "已完成") if self.tasks_tree else 0
        self.stats_label.config(text=f"总任务: {total_tasks}\n已完成: {completed_tasks}")
        if  not self.shutdown_after_download_btn.get_state():
            self.available_shutdown = False
        if  total_tasks != 0 and total_tasks != completed_tasks and self.shutdown_after_download_btn.get_state():
            self.available_shutdown = True
        if  total_tasks != 0 and total_tasks == completed_tasks and self.shutdown_after_download_btn.get_state() and self.available_shutdown:
            subprocess.Popen(
                'shutdown /s /t 0', shell=True,
                stdin =subprocess.DEVNULL,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
        self.root.after(500, self.stats_update)

    def update_video_info(self, info):
        def safe_filename(filename):
            return re.sub(r'[\\/:*?"<>|“‘’”？。…]', '', filename).replace('&quot;','')
        self.title_var.set(info['title'])
        self.name_var.set(safe_filename(info['title']))
        self.size_var.set(f"{info['size'][0] / 1024 / 1024:.2f}MB (视频) + {info['size'][1] / 1024 / 1024:.2f}MB (音频)")
        self.dpi_var .set(f"{info['dpi'][0]}x{info['dpi'][1]}")
        if self.auto_download_video_var:threading.Thread(target=self.download,args=(True ,app.config_data.get('auto_download_overwrite_without_ask', False)),daemon=True).start()
        if self.auto_download_audio_var:threading.Thread(target=self.download,args=(False,app.config_data.get('auto_download_overwrite_without_ask', False)),daemon=True).start()
    
    def browse_path(self):
        path = filedialog.askdirectory()
        if  path:
            self.path_var.set(path.replace('/', '\\'))
    
    def download(self, is_video, overwrite_without_ask:bool=False):
        path = self.path_var.get()
        name = self.name_var.get()

        if not path:messagebox.showerror("错误", "请选择保存路径");return
        if not name:messagebox.showerror("错误", "请输入文件名"  );return
        if len(name) > 255:messagebox.showerror("错误", "文件名长度不能超过255个字符");return
        if name[0]  == ' ':messagebox.showerror("错误", "文件名不能以空格开头"       );return
        if re.search(r'[\\/:*?"<>|]', name) is not None:
            messagebox.showerror("错误", "文件名不能包含以下字符: \\ / : * ? \" < > |")
            return
        if os.path.exists(os.path.join(path, f"{name}.mp4" if is_video else f"{name}.mp3")):
            if os.path.isdir(os.path.join(path, f"{name}.mp4" if is_video else f"{name}.mp3")):
                messagebox.showerror("错误", "文件名与目录名冲突")
                return
            if not overwrite_without_ask:
                if not messagebox.askyesno("提示", "{} 已存在，是否覆盖？".format(f"{name}.mp4" if is_video else f"{name}.mp3")):return

        self.create_task(path, name, is_video);self.show_tasks_page()

    def create_task(self, path:str, name:str, is_video:bool, info=None):
        if not path or not name:return
        
        task_id   = str(self.tasks_CMID);self.tasks_CMID += 1
        task_type = "视频" if is_video else "音频"
        self.tasks_tree.insert("", "end", task_id, 
                              values=(name, task_type, "0%", "等待中"))
        def download_thread():
            self.downloader.download(
                path=path,
                info=self.downloader.current_info if info is None else info,
                file_name=name,
                only_audio=not is_video,
                progressFunc=self.update_progress,
                taskID=task_id
            )
        self.tasks_thread.update({task_id: threading.Thread(target=download_thread, daemon=True)})

    def download_manager(self):
        maximum_threads = self.maximum_threads_var.get()
        current_threads = 0
        try:
            for item in self.tasks_tree.get_children():
                if  self.tasks_tree.item(item, "values")[3] == "下载中" or \
                    self.tasks_tree.item(item, "values")[3] == "准备中":
                    current_threads += 1
                if  current_threads >= maximum_threads:break
                if  self.tasks_tree.item(item, "values")[3] == "等待中":
                    self.tasks_thread[item].start()
                    current_threads += 1
                if  current_threads >= maximum_threads:break
        except:pass
        self.root.after(200, self.download_manager)
    
    def update_progress(self, total, current, taskID, success:bool=False, info=None):
        if  not self.tasks_tree.exists(taskID):
            return
        if  not success and info is None:
            progress = f"{current / total * 100:.1f}%"
            self.tasks_tree.set(taskID, "progress", progress)
            if  self.tasks_tree.item(taskID, "values")[3] != "暂停中":
                self.tasks_tree.set (taskID, "status", "下载中" if current < total else "文件转换中")
        elif success:
            self.tasks_tree.set(taskID, "progress", f"{current / total * 100:.1f}%")
            self.tasks_tree.set(taskID, "status", "已完成" if current == total else "文件转换中")
        else:
            self.tasks_tree.set(taskID, "status", f"失败: {info}")
    
    def manage_task(self, task_id=None):
        if task_id is not None:
            selected = [task_id]
        else:
            selected = self.tasks_tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        task_id = selected[0]
        status = self.tasks_tree.item(task_id, "values")[3]
        
        if status == "暂停中":
            self.tasks_tree.set(task_id, "status", "下载中")
        elif status == "下载中":
            self.tasks_tree.set(task_id, "status", "暂停中")
        else:
            messagebox.showinfo("提示", "只能暂停或继续正在下载的任务")
    
    def delete_task(self, task_id=None):
        if task_id is not None:
            selected = [task_id]
        else:
            selected = self.tasks_tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        for task_id in selected:
            self.tasks_tree.delete(task_id)

    def delete_done(self):
        for item in self.tasks_tree.get_children():
            if  self.tasks_tree.item  (item, "values")[3] == "已完成":
                self.tasks_tree.delete(item)
                
    def open_download_folder(self, task_id=None):
        if task_id is not None:
            path = self.downloader.download_file_path[task_id][0]
            if os.path.isdir(path):
                subprocess.Popen('explorer "{}"'.format(path))
            else:
                messagebox.showerror("错误", "下载路径不存在")
            return
        selected = self.tasks_tree.selection();path=self.downloader.download_file_path[selected[0]][0] if selected else self.path_var.get()
        if os.path.isdir(path):
            subprocess.Popen('explorer "{}"'.format(path))
        else:
            messagebox.showerror("错误", "下载路径不存在")

    def open_file(self, task_id=None):
        if task_id is not None:
            selected = [task_id]
        else:
            selected = self.tasks_tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一个任务")
            return
        
        file_path = self.downloader.download_file_path[selected[0]]
        file_path = os.path.join(file_path[0], file_path[1])
        if not os.path.isfile(file_path):
            messagebox.showerror("错误", "文件不存在")
            return
        
        subprocess.Popen('explorer "{}"'.format(file_path))

def add_right_click_menu_to_entry(entry_widget):
    """
    为现有的Entry控件快速添加右键菜单功能（复制、剪切、粘贴）
    
    参数:
        entry_widget: 要添加右键菜单的Entry控件
    """
    def copy_from_entry(entry):
        """复制选中的文本"""
        if entry.selection_present():
            selected_text = entry.selection_get()
            entry.master.clipboard_clear()
            entry.master.clipboard_append(selected_text)

    def cut_from_entry(entry):
        """剪切选中的文本"""
        if entry.selection_present():
            selected_text = entry.selection_get()
            entry.master.clipboard_clear()
            entry.master.clipboard_append(selected_text)
            entry.delete(tk.SEL_FIRST, tk.SEL_LAST)

    def paste_to_entry(entry):
        """粘贴文本到光标位置"""
        try:
            clipboard_text = entry.master.clipboard_get()
            if entry.selection_present():
                entry.delete(tk.SEL_FIRST, tk.SEL_LAST)
            entry.insert(tk.INSERT, clipboard_text)
        except tk.TclError:
            pass  # 剪贴板为空或内容不是文本

    def show_menu(event, menu):
        """在鼠标位置显示右键菜单"""
        menu.post(event.x_root, event.y_root)

    # 创建右键菜单
    menu = tk.Menu(entry_widget, tearoff=0)
    menu.add_command(label="复制", command=lambda: copy_from_entry(entry_widget))
    menu.add_command(label="剪切", command=lambda: cut_from_entry(entry_widget))
    menu.add_command(label="粘贴", command=lambda: paste_to_entry(entry_widget))
    
    # 绑定右键事件
    entry_widget.bind("<Button-3>", lambda event: show_menu(event, menu))
    
    # 返回菜单对象，以便需要时可以进一步自定义
    return menu

def main():
    # 为Canvas添加创建圆角矩形的方法
    def create_rounded_rect(self, x1, y1, x2, y2, radius=10, **kwargs):
        points = [x1+radius, y1,
                x2-radius, y1,
                x2, y1,
                x2, y1+radius,
                x2, y2-radius,
                x2, y2,
                x2-radius, y2,
                x1+radius, y2,
                x1, y2,
                x1, y2-radius,
                x1, y1+radius,
                x1, y1]
        return self.create_polygon(points, smooth=True, **kwargs)
    # 将方法添加到Canvas类
    tk.Canvas.create_rounded_rect = create_rounded_rect

    global root,app
    root = tk.Tk()
    app  = BiliBiliDownloader(root)
    try   :root.mainloop()
    except:pass

if __name__ == '__main__':main()